/**
 * File: GuiEvent.js
 * Description:
 * License:
 * 
 * @author Andrea Ballatore
 */

/** constants */
_EVENT_TYPE_MAP_STARTED = "map_started";
_EVENT_TYPE_PAGE_STARTED = "page_started";
_EVENT_TYPE_LAYERS_CHANGED = "changelayer";

var _MOUSE_BUTTON_LEFT 	= "lmouse";
var _MOUSE_BUTTON_RIGHT = "rmouse";
var _MOUSE_BUTTON_MIDDLE= "mmouse";
var _LAYER_TYPE_OVERLAY = "overlay";
var _LAYER_TYPE_BASE 	= "base";


/**
 * Constructor of a GuiEvent.
 * The GuiEvent represents a simplified DOM event
 * containing only the relevant information 
 * (e.g. timestamp, mouse location, etc). 
 * @return void
 */
function GuiEvent() {
	
	this.type = "";
	
	/** client timestamps */
	this.clientTimestamp = new Date().getTime();
	this.clientEndTimestamp = null;
		
	/** info about event in relation to OpenLayers map */
	this.mapInfo = null;
	/** info about mouse in relation to OpenLayers map */
	this.mouseInfo = null;
	this.currentTarget = null;
	this.layerInfo = null;
	this.pageInfo = null;
	this.touchInfo = null;
};

GuiEvent.prototype.isEventLayer = function() {
	assert( this.type, 'isEventLayer: type null' );
	var layerTypes = new Array( 
		"preaddlayer",
		"addlayer",
		"removelayer",
		"changelayer"
		);
	return layerTypes.indexOf( this.type.toLowerCase() )>=0;
};

GuiEvent.prototype.isEventMouseMove = function() {
	assert( this.type, 'isEventClick: type null' );
	var clickTypes = new Array( 
		"mousemove",
		"mouseout",
		"mouseover"
		);
	return clickTypes.indexOf( this.type.toLowerCase() )>=0;
};

GuiEvent.prototype.isEventMapMove = function() {
	assert( this.type, 'isEventMapMove: type null' );
	var clickTypes = new Array( 
		"movestart",
		"move",
		"moveend",
		"zoomend"
		);
	return clickTypes.indexOf( this.type.toLowerCase() )>=0;
};

/**
 * iPhone events
 * @return boolean
 */
GuiEvent.prototype.isEventTouch = function() {
	assert( this.type, 'isEventTouch: type null' );
	var clickTypes = new Array( 
		"touchstart",
		"touchmove",
		"touchend"
		);
	return clickTypes.indexOf( this.type.toLowerCase() )>=0;
};

GuiEvent.prototype.isEventClick = function() {
	assert( this.type, 'isEventClick: type null' );
	var clickTypes = new Array( 
		"click",
		"dbclick",
		"mousedown",
		"mouseup"
	);
	return clickTypes.indexOf( this.type.toLowerCase() )>=0;
};

GuiEvent.prototype.isEventScroll = function() {
	assert( this.type, 'isEventScroll: type null' );
	var clickTypes = new Array( 
		"dommousescroll",
		"mousewheel"
	);
	return clickTypes.indexOf( this.type.toLowerCase() )>=0;
};

GuiEvent.prototype.isEventPageLayout = function() {
	assert( this.type, 'isEventPageLayout: type null' );
	var clickTypes = new Array( 
		"scroll",
		_EVENT_TYPE_PAGE_STARTED
	);
	return clickTypes.indexOf( this.type.toLowerCase() )>=0;
};

GuiEvent.prototype.isEventMapStarted = function() {
	assert( this.type, 'isEventMapStarted: type null' );
	var clickTypes = new Array( 
			_EVENT_TYPE_MAP_STARTED
		);
	return clickTypes.indexOf( this.type.toLowerCase() )>=0;
};

/**
 * @param eventType
 * @return TRUE if the eventType is valid
 */
GuiEvent.prototype.isEventTypeValid = function(){
	assert( this.type, 'isEventTypeValid: type null' );
	
	return this.isEventClick() 
		|| this.isEventMouseMove() 
		|| this.isEventLayer()
		|| this.isEventMapMove()
		|| this.isEventMapStarted()
		|| this.isEventScroll()
		|| this.isEventPageLayout()
		|| this.isEventMapMove()
		|| this.isEventTouch();
};

/**
 * Works out and update speed of screen event (if relevant).
 * The speed is expressed in px/ms
 * 
 * @param previousEventWithLocation
 * @return void 
 */
GuiEvent.prototype.updateScreenSpeed = function( previousEventWithLocation ){
	assert( this.isEventTypeValid(), 'updateScreenSpeed: invalid event' );
	
	if (!previousEventWithLocation){
		// default speed 
		this.mouseInfo.speed = 0;
		return;
	}
	
	assert( previousEventWithLocation, "updateScreenSpeed: previousEventWithLocation is null" );
	assert( previousEventWithLocation.getPageLocation(), "updateScreenSpeed: previousEventWithLocation.getScreenLocation is null" );
	// work out speed
	var dDistance = _getDistanceBetween2Points( previousEventWithLocation.getPageLocation(), this.getPageLocation() );
	
	var dTimeMs = this.clientTimestamp - previousEventWithLocation.clientTimestamp;
	var fTolerance = 0.0000001;
	assert( dTimeMs + fTolerance >= 0, "updateScreenSpeed: dTimeMs < 0: " + dTimeMs );
	var dSpeed = dDistance / dTimeMs;
	//log.debug("dDistance: "+dDistance + " dTimeMs: "+dTimeMs+ " speed="+dSpeed);
	
	this.mouseInfo.speed = dSpeed;
};

/**
 * @return screen location or null 
 */
GuiEvent.prototype.getPageLocation = function( previousEvent ){
	assert( this.isEventTypeValid(), 'getScreenLocation: invalid event' );
	if (this.mouseInfo && this.mouseInfo.pageX && _isNumber(this.mouseInfo.pageX)){
		var location = new Object();
		location.x = this.mouseInfo.pageX;
		location.y = this.mouseInfo.pageY;
		assert( location.x >= 0, "getPageLocation: invalid x" );
		assert( location.y >= 0, "getPageLocation: invalid y" );
		return location;
	}
	return null;
};

/**
 * @param type
 * return TRUE if the type is valid
 */
function _isEventTypeValid( type ){
	assert( type, '_isEventTypeValid: type null' );
	var temp = new GuiEvent();
	temp.type = type.toLowerCase().trim();
	return temp.isEventTypeValid();
}

/**
 * @param event mouse event
 * @param mapDivId id of the html div where the map is deployed.
 *  
 * return TRUE if the event happened on a map
 */
function _isEventOnMap( event, mapDivId ){
	assert( event, '_isEventOnMap: event null' );
	assert( mapDivId, '_isEventOnMap: mapDivId null' );

	var targetId = null;
	if (event && event.element && event.element.id)
		targetId = event.element.id;
	if (!targetId){
		//log.warn("_isEventOnMap: targetId null");
		return false;
	}
	
	if (   targetId.toUpperCase().trim() 
		== mapDivId.toUpperCase().trim() ){
		return true;
	}
	// test against GeoEXT div
	if ( _matchString("ext-gen",targetId) ) 
		return true;
	
	return false;
}

/**
 * Constructor of GuiEventPacket. The packet contains at most maxSize
 * GuiEvent and has an index (unique for the current MapLogger).
 * @param index index of the packet
 * @param maxSize max number of GuiEvent
 * @return void
 */
function GuiEventPacket( index, maxSize ) {
	this.eventsQueue = new Array();
	this.index = index;
	assert( maxSize > 0, "GuiEventPacket: maxSize <= 0" );
	assert( maxSize < 1000000, "GuiEventPacket: maxSize > 1000000" );
	this.maxSize = maxSize;
}

/**
 * 
 * @param guiEvent GuiEvent to be added to the packet
 * @return void
 */
GuiEventPacket.prototype.add = function( guiEvent ){
	assert( guiEvent instanceof GuiEvent, "GuiEventPacket.prototype.add wrong type" );
	assert( !this.isFull(), "GuiEventPacket full");
	
	this.eventsQueue.push( guiEvent );
};

/**
 * @return true if the packet is full
 */
GuiEventPacket.prototype.isFull = function(){
	assert( this.eventsQueue.size() <= this.maxSize, "GuiEventPacket isFull: "+this.eventsQueue.size()+" is bigger than maxSize "+ this.maxSize );
	return ( this.eventsQueue.size() == this.maxSize);
};

/**
 * Update ending time of events
 * @return void
 */
GuiEventPacket.prototype.updateEndingTimes = function(){
	assert(this.isFull(),"updateEndingTimes: eventsQueue is not full.");
	
	var previousEvent = null;
	for (i=0; i < this.eventsQueue.size(); i++ ){
		assert( this.eventsQueue[i], "updateEndingTimes: eventsQueue obj null" );
		if (previousEvent){
			assert( this.eventsQueue[i].clientTimestamp >= previousEvent.clientTimestamp, "updateEndingTimes: events in wrong order");
			// all the other events but the first
			previousEvent.clientEndTimestamp = this.eventsQueue[i].clientTimestamp;
		}
		if (this.eventsQueue.size() == i+1){
			// last event in packet
			if ( !this.eventsQueue[i].clientEndTimestamp ){
				this.eventsQueue[i].clientEndTimestamp = new Date().getTime();
			}
		}
		previousEvent = this.eventsQueue[i];
	}
	for (i=0; i < this.eventsQueue.size(); i++ ){
		assert( this.eventsQueue[i], "updateEndingTimes: eventsQueue obj null" );
		assert( this.eventsQueue[i].clientEndTimestamp >= this.eventsQueue[i].clientTimestamp, "updateEndingTimes: wrong time for clientEndTimestamp2: "+this.eventsQueue[i]);
	}
};

/**
 * @return current packet size
 */
GuiEventPacket.prototype.size = function(){
	return ( this.eventsQueue.size() );
};

/**
 * @return a JSON string representing the GuiEvents in the packet
 */
GuiEventPacket.prototype.toJSON = function(){
	//return '{"eventsQueue":[{"clientDate":1265643906771,"map":true,"lon":-6.2191926402490285,"lat":53.310364781688044,"currentTarget":{"_eventCacheID":"map_eventCacheID_3","scrolls":"[0, 0]","lefttop":"[0, 0]","offsets":"[34, 122]","hasScrollEvent":true},"type":"mousemove","pageX":577,"pageY":172,"screenX":1995,"screenY":402,"layerX":542,"layerY":50}]}';
	
	JSONtxt = '{"eventsQueue":[';
	for (i=0; i < this.eventsQueue.size(); i++ ){
		assert( this.eventsQueue[i], "toJSON eventsQueue null" );
		assert( this.eventsQueue[i] instanceof GuiEvent, "toJSON wrong type" );
		try {
			JSONelem = GuiEventToJSON( this.eventsQueue[i] );
			
			assert( JSONelem,"JSONelem toJSON null");
			if (JSONelem){
				JSONtxt += JSONelem;
			
				// skip last elem
				if (i<this.eventsQueue.size()-1 )
					JSONtxt += ',';
			}
		} catch (e) {
			log.error("GuiEventPacket.toJSON GuiEventToJSON error");
			log.error(e);
		}

	}
	JSONtxt += ']}';
	assert( JSONtxt.length < 10000000, "toJSON too big")
	 
	// returns JSON array
	return JSONtxt;
};

/**
 * Converts a GuiEvent into a JSON string.
 * @param guiEvent GuiEvent to be converted into JSON
 * @return a valid JSON string or null if something went wrong
 */
function GuiEventToJSON( guiEvent ){
	// check for JSON library
	var bNativeJSON = false;
	var bPrototypeJSON = false;
	
	if ( typeof(JSON)=='object' )
		bNativeJSON = true;
	
	if (!bNativeJSON){
		bPrototypeJSON = (typeof Object.toJSON)=='function';
	}
	if (!bNativeJSON && !bPrototypeJSON){
		_alertBrowserNotSupported("JSON library not found!");
		return null;
	}
	
	try{
		assert( guiEvent instanceof GuiEvent, "GuiEventToJSON wrong type" );
		
		// most browsers have this function
		if ( bNativeJSON ){
			var jsonString = JSON.stringify( guiEvent );
			return jsonString;
		}
		if ( bPrototypeJSON ){
			// mobile safari
			assert( (typeof Object.toJSON)=='function', "GuiEventToJSON: Object.toJSON undefined.");
			var jsonString = Object.toJSON( guiEvent );
			return jsonString;
		}
		// should never get here
		return null;
	} catch (e) {
		log.error("GuiEventToJSON - error");
		log.error(e);
		log.error("caused by event: ");
		log.error(guiEvent);
		return null;
	}
}

/**
 * Factory of GuiEvent.
 * 
 * Note: this conversion is a crucial component of the system. 
 * Given that the GuiEvent has to be converted into JSON, cyclical references can
 * break the process.
 * 
 * @param event a DOM event coming directly from the Web Browser
 * @param mapRenderer MapRenderer mapRenderer (can be null if the event didn't happen on the map)
 * @return a GuiEvent containing a simplified representation of event, or null if something went wrong.
 */
function GuiEventFactory( event, mapRenderer ){
	assert(event,"GuiEventFactory: event null");
	
	if (mapRenderer){
		assert( mapRenderer instanceof MapRenderer, "invalid mapRenderer" );
		assert( mapRenderer.olMap instanceof OpenLayers.Map, "invalid openLayersMap" );	
	}
	
	// valid event, let's build it
	var guiEvent = new GuiEvent();
	guiEvent.type 	= event.type;
	// clear string
	if (guiEvent.type){
		guiEvent.type = guiEvent.type.trim().toLowerCase();
		
		// reset type for dommousescroll
		if (guiEvent.type == "dommousescroll")
			guiEvent.type = "mousewheel";
	}

	// determine whether the event happened on the map
	if (mapRenderer){
		guiEvent.bOnMap = _isEventOnMap( event, mapRenderer.mapHtmlDivId );
		if (guiEvent.isEventMapStarted() || guiEvent.isEventLayer()){
			guiEvent.bOnMap = true;
		}
	} else {
		// no maprenderer, therefore
		guiEvent.bOnMap = false;
	}

	/* ============== mouse page location ============== */
	if ( guiEvent.isEventMouseMove() || guiEvent.isEventClick() || guiEvent.isEventScroll()){
		// read page events
		guiEvent.mouseInfo = new Object();
		
		assert(_exists(event.pageX),	'GuiEventFactory: event.pageX null - type: '+guiEvent.type);
		assert(_exists(event.screenX),	'GuiEventFactory: event.screenX null - type: '+guiEvent.type);
		// layerX is optional, therefore not asserted
		
		guiEvent.mouseInfo.pageX 	= event.pageX;
		guiEvent.mouseInfo.pageY 	= event.pageY;
		guiEvent.mouseInfo.screenX 	= event.screenX;
		guiEvent.mouseInfo.screenY 	= event.screenY;
		if (event.layerX){
			guiEvent.mouseInfo.layerX 	= event.layerX;
			guiEvent.mouseInfo.layerY 	= event.layerY;
		}
	}
	
	/* ============== iPhone events ============== */
	if ( guiEvent.isEventTouch() ){
		// read page events
		guiEvent.touchInfo = new Object();
		// loop through touch list
		if (event.touches.length > 0){
			guiEvent.touchInfo.touches = new Array();
			var i = 0;
			for(i = 0; i < event.touches.length; i ++ ){
				var curTouch = event.touches[i];
				assert(_isNumber( curTouch.pageX ),'GuiEventFactory: curTouch.pageX null');
				assert(_isNumber( curTouch.screenX ), 'GuiEventFactory: curTouch.screenX null');
				
				// build new finger
				var newFinger = new Object();
				newFinger.id = i;
				newFinger.clientTimestamp = guiEvent.clientTimestamp;
				newFinger.pageX 	= curTouch.pageX;
				newFinger.pageY 	= curTouch.pageY;
				newFinger.screenX 	= curTouch.screenX;
				newFinger.screenY 	= curTouch.screenY;
				/*
				if (guiEvent.bOnMap){
					assert(mapRenderer,"GuiEventFactory: touch event, mapRenderer is null");
					var mousePos = mapRenderer.getLatLonMousePos( newFinger );
					/** current location of the pointer on the map */
				//	newFinger.geoLoc = new Object();
				//	newFinger.geoLoc.lon = mousePos.lon;
				//	newFinger.geoLoc.lat = mousePos.lat;
				//	log.warn(newFinger.geoLoc.lon + " " + newFinger.geoLoc.lat);
				//}
				guiEvent.touchInfo.touches.push( newFinger );
			}
		}
		
	}
	/* ============== scroll ============== */
	if ( guiEvent.isEventScroll() ){
		if (event.detail !=0 ){
			// invert value
			guiEvent.mouseInfo.wheelDirection = - event.detail;
		} else {
			// get wheelDelta
			guiEvent.mouseInfo.wheelDirection = event.wheelDelta;
		}
		assert(guiEvent.mouseInfo.wheelDirection,'GuiEventFactory: wheelDirection null');
	}
	
	/* ============== page layout ============== */
	/** something changed in the page layout, get info */ 
	if ( guiEvent.isEventPageLayout()){
		// get page info
		guiEvent.pageInfo = new Object();
		
		guiEvent.pageInfo.width  = _getPageSizeXY()[0];
		guiEvent.pageInfo.height = _getPageSizeXY()[1];
		guiEvent.pageInfo.horizontalScroll  = _getPageScrollXY()[0];
		guiEvent.pageInfo.verticalScroll 	= _getPageScrollXY()[1];
	}
	
	/** read info for specific event types */
	if ( guiEvent.isEventLayer() ){
		assert( event.layer,'GuiEventFactory: event.layer null' );
		guiEvent.layerInfo = new Object();
		guiEvent.layerInfo.name = event.layer.name;
		guiEvent.layerInfo.description = event.layer.id;
		guiEvent.layerInfo.property = event.property;
		guiEvent.layerInfo.visibility = event.layer.visibility;
		
		if (event.layer_type == _LAYER_TYPE_BASE){
			// base OSM layers
			guiEvent.layerInfo.layer_type = _LAYER_TYPE_BASE;
			guiEvent.layerInfo.active = event.activelayers; 
		} else {
			// overlays 
			assert(!guiEvent.layerInfo.layer_type,"GuiEventFactory: layerInfo.layer_type expected to be null but has a value.");
			guiEvent.layerInfo.layer_type = _LAYER_TYPE_OVERLAY;
		}
	}
	
	if ( guiEvent.isEventClick() ){
		// set currentTarget
		// NOTE: currentTarget can cause dangerous cyclic references
		if (event.currentTarget){
			guiEvent.currentTarget = new Object();
			guiEvent.currentTarget.id = event.currentTarget.id;
			guiEvent.currentTarget.className = event.currentTarget.className;
			guiEvent.currentTarget.nodeName = event.currentTarget.nodeName;
		}
		
		if ( _isMicrosoftInternetExplorer() ){
			log.debug("fix for IE");
			if (event.button == 1)  guiEvent.mouseInfo.button = _MOUSE_BUTTON_LEFT;
			if (event.button == 4) 	guiEvent.mouseInfo.button = _MOUSE_BUTTON_MIDDLE;
			if (event.button == 2)  guiEvent.mouseInfo.button = _MOUSE_BUTTON_RIGHT;
		} else {
			// standard browsers
			if (event.button == 0)  guiEvent.mouseInfo.button = _MOUSE_BUTTON_LEFT;
			if (event.button == 1) 	guiEvent.mouseInfo.button = _MOUSE_BUTTON_MIDDLE;
			if (event.button == 2)  guiEvent.mouseInfo.button = _MOUSE_BUTTON_RIGHT;
		}
		assert( guiEvent.mouseInfo.button, "GuiEventFactory: unknown mouse button: "+ event.button);
	}
	
	/* ============== map events ============== */
	if (guiEvent.bOnMap){
		if (guiEvent.isEventMouseMove() || guiEvent.isEventClick()){
			if (event.xy){
				// get mouse geo location
				var mousePos = mapRenderer.getLatLonMousePos( event );
				/** current location of the pointer on the map */
				guiEvent.mouseInfo.geoLoc = new Object();
				guiEvent.mouseInfo.geoLoc.lon = mousePos.lon;
				guiEvent.mouseInfo.geoLoc.lat = mousePos.lat;
				
				assert(_exists(event.xy.x),'GuiEventFactory: event.xy.x');
				guiEvent.mouseInfo.mapScreenLocation = new Object();
				guiEvent.mouseInfo.mapScreenLocation.x = event.xy.x;
				guiEvent.mouseInfo.mapScreenLocation.y = event.xy.y;
			}
		}
		/* ============== map viewport/bounds ============== */
		if (guiEvent.isEventMapMove() || guiEvent.isEventMapStarted())
		{
			guiEvent.mapInfo = new Object();
			guiEvent.mapInfo.mapBounds = new Object();
			guiEvent.mapInfo.mapBoundsMerc = new Object();
	
			assert( mapRenderer,"GuiEventFactory: mapRenderer null" );
			var bounds = mapRenderer.getLatLonBounds();
			var boundsMerc = mapRenderer.getMercBounds();
			
			// save latlon bounds
		    guiEvent.mapInfo.mapBounds.left = bounds.left;
		    guiEvent.mapInfo.mapBounds.top = bounds.top;
		    guiEvent.mapInfo.mapBounds.bottom = bounds.bottom;
		    guiEvent.mapInfo.mapBounds.right = bounds.right;
		    
			// save merc bounds
		    guiEvent.mapInfo.mapBoundsMerc.left = boundsMerc.left;
		    guiEvent.mapInfo.mapBoundsMerc.top = boundsMerc.top;
		    guiEvent.mapInfo.mapBoundsMerc.bottom = boundsMerc.bottom;
		    guiEvent.mapInfo.mapBoundsMerc.right = boundsMerc.right;
		    
		    guiEvent.mapInfo.numZoomLevels 	= mapRenderer.olMap.numZoomLevels;
			guiEvent.mapInfo.curZoomLevel 	= mapRenderer.olMap.getZoom();
			guiEvent.mapInfo.size 			= mapRenderer.olMap.size;
			guiEvent.mapInfo.curScale 		= mapRenderer.olMap.getScale();
		}
	}
	return guiEvent;
}